5 // Created by Evan Schoenberg on 11/23/05.
8 #import <Adium/AIStatusMenu.h>
9 #import <Adium/AIStatus.h>
10 #import <Adium/AIStatusGroup.h>
11 #import <Adium/AIAccount.h>
12 #import <Adium/AIStatusControllerProtocol.h>
13 #import <Adium/AIEditStateWindowController.h>
14 #import <Adium/AIStatusIcons.h>
15 #import <Adium/AIAccountControllerProtocol.h>
16 #import <Adium/AIMenuControllerProtocol.h>
17 #import <Adium/AIPreferenceControllerProtocol.h>
18 #import <AIUtilities/AIArrayAdditions.h>
19 #import <AIUtilities/AIEventAdditions.h>
20 #import <AIUtilities/AIMenuAdditions.h>
21 #import <AIUtilities/AIStringAdditions.h>
23 #define STATUS_TITLE_CUSTOM [AILocalizedString(@"Custom", nil) stringByAppendingEllipsis]
24 #define STATE_TITLE_MENU_LENGTH 30
26 @interface AIStatusMenu (PRIVATE)
27 - (id)initWithDelegate:(id)inDelegate;
30 @implementation AIStatusMenu
32 + (id)statusMenuWithDelegate:(id)inDelegate
34 return [[[self alloc] initWithDelegate:inDelegate] autorelease];
37 - (id)initWithDelegate:(id)inDelegate
39 if ((self = [super init])) {
40 delegate = inDelegate;
42 NSParameterAssert([delegate respondsToSelector:@selector(statusMenu:didRebuildStatusMenuItems:)]);
44 menuItemArray = [[NSMutableArray alloc] init];
45 stateMenuItemsAlreadyValidated = [[NSMutableSet alloc] init];
49 [[adium notificationCenter] addObserver:self
50 selector:@selector(stateArrayChanged:)
51 name:AIStatusStateArrayChangedNotification
53 [[adium notificationCenter] addObserver:self
54 selector:@selector(activeStatusStateChanged:)
55 name:AIStatusActiveStateChangedNotification
58 //Update our state menus when the state array or status icon set changes
59 [[adium notificationCenter] addObserver:self
60 selector:@selector(statusIconSetChanged:)
61 name:AIStatusIconSetDidChangeNotification
70 [[adium notificationCenter] removeObserver:self];
71 [stateMenuItemsAlreadyValidated release];
72 [menuItemArray release];
79 - (void)setDelegate:(id)inDelegate
81 delegate = inDelegate;
85 * @brief The delegate is just too good for the menu items we've created; it will create all of the ones it wants on its own
87 - (void)delegateWillReplaceAllMenuItems
89 //Remove the menu items from needing update
90 [stateMenuItemsAlreadyValidated removeAllObjects];
92 //Clear the array itself
93 [menuItemArray removeAllObjects];
97 * @brief The delegate created its own menu items it wants us to track and update
99 - (void)delegateCreatedMenuItems:(NSArray *)addedMenuItems
101 //Now add the items we were given
102 [menuItemArray addObjectsFromArray:addedMenuItems];
105 - (void)stateArrayChanged:(NSNotification *)notification
110 - (void)activeStatusStateChanged:(NSNotification *)notification
112 [stateMenuItemsAlreadyValidated removeAllObjects];
115 - (void)statusIconSetChanged:(NSNotification *)notification
121 * @brief Generate the custom menu item for a status type
123 - (NSMenuItem *)customMenuItemForStatusType:(AIStatusType)statusType
125 NSMenuItem *menuItem;
127 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
129 action:@selector(selectCustomState:)
132 [menuItem setImage:[AIStatusIcons statusIconForStatusName:nil
133 statusType:statusType
134 iconType:AIStatusIconMenu
135 direction:AIIconNormal]];
136 [menuItem setTag:statusType];
138 return [menuItem autorelease];
143 * @brief Add state menu items
145 * Adds all the necessary state menu items to a plugin's state menu
146 * @param stateMenuPlugin The state menu plugin we're updating
150 NSEnumerator *enumerator;
151 NSMenuItem *menuItem;
152 AIStatus *statusState;
153 AIStatusType currentStatusType = AIAvailableStatusType;
154 AIStatusMutabilityType currentStatusMutabilityType = AILockedStatusState;
156 [[adium menuController] delayMenuItemPostProcessing];
158 if ([delegate respondsToSelector:@selector(statusMenu:willRemoveStatusMenuItems:)]) {
159 [delegate statusMenu:self willRemoveStatusMenuItems:menuItemArray];
162 [menuItemArray removeAllObjects];
163 [stateMenuItemsAlreadyValidated removeAllObjects];
165 /* Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
166 * are grouped together.
168 enumerator = [[[adium statusController] sortedFullStateArray] objectEnumerator];
169 while ((statusState = [enumerator nextObject])) {
170 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
171 AIStatusType thisStatusType = [statusState statusType];
172 AIStatusType thisStatusMutabilityType = [statusState mutabilityType];
174 if ((currentStatusMutabilityType != AISecondaryLockedStatusState) &&
175 (thisStatusMutabilityType == AISecondaryLockedStatusState)) {
176 //Add the custom item, as we are ending this group
177 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
179 //Add a divider when we switch to a secondary locked group
180 [menuItemArray addObject:[NSMenuItem separatorItem]];
183 //We treat Invisible statuses as being the same as Away for purposes of the menu
184 if (thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
186 /* Add the "Custom..." state option and a separatorItem before beginning to add items for a new statusType
187 * Sorting the menu items before enumerating means that we know our statuses are sorted first by statusType
189 if ((currentStatusType != thisStatusType) &&
190 (currentStatusType != AIOfflineStatusType)) {
192 //Don't include a Custom item after the secondary locked group, as it was already included
193 if ((currentStatusMutabilityType != AISecondaryLockedStatusState)) {
194 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
198 [menuItemArray addObject:[NSMenuItem separatorItem]];
200 currentStatusType = thisStatusType;
203 menuItem = [[NSMenuItem alloc] initWithTitle:[AIStatusMenu titleForMenuDisplayOfState:statusState]
205 action:@selector(selectState:)
208 if ([statusState isKindOfClass:[AIStatus class]]) {
209 [menuItem setToolTip:[statusState statusMessageTooltipString]];
213 [menuItem setSubmenu:[(AIStatusGroup *)statusState statusSubmenuNotifyingTarget:self
214 action:@selector(selectState:)]];
216 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
217 forKey:@"AIStatus"]];
218 [menuItem setTag:currentStatusType];
219 [menuItem setImage:[statusState menuIcon]];
220 [menuItemArray addObject:menuItem];
223 currentStatusMutabilityType = thisStatusMutabilityType;
227 if (currentStatusType != AIOfflineStatusType) {
228 /* Add the last "Custom..." state optior for the last statusType we handled,
229 * which didn't get a "Custom..." item yet. At present, our last status type should always be
230 * our AIOfflineStatusType, so this will never be executed and just exists for completeness.
232 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
235 //Now that we are done creating the menu items, tell the plugin about them
236 [delegate statusMenu:self didRebuildStatusMenuItems:menuItemArray];
238 [[adium menuController] endDelayMenuItemPostProcessing];
242 * @brief Menu validation
244 * Our state menu items should always be active, so always return YES for validation.
246 * Here we lazily set the state of our menu items if our stateMenuItemsAlreadyValidated set indicates it is needed.
248 * Random note: stateMenuItemsAlreadyValidated will almost never have a count of 0 because separatorItems
249 * get included but never get validated.
251 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
253 if (![stateMenuItemsAlreadyValidated containsObject:menuItem]) {
254 NSDictionary *dict = [menuItem representedObject];
255 AIAccount *account = [dict objectForKey:@"AIAccount"];
256 AIStatus *menuItemStatusState = [dict objectForKey:@"AIStatus"];
259 /* Account-specific menu items */
260 AIStatus *appropiateActiveStatusState;
261 appropiateActiveStatusState = [account statusState];
263 /* Our "Custom..." menu choice has a nil represented object. If the appropriate active search state is
264 * in our array of states from which we made menu items, we'll be searching to match it. If it isn't,
265 * we have a custom state and will be searching for the custom item of the right type, switching all other
266 * menu items to NSOffState.
268 if ([[[adium statusController] flatStatusSet] containsObject:appropiateActiveStatusState]) {
269 //If the search state is in the array so is a saved state, search for the match
270 if ((menuItemStatusState == appropiateActiveStatusState) ||
271 ([menuItemStatusState isKindOfClass:[AIStatusGroup class]] &&
272 [(AIStatusGroup *)menuItemStatusState enclosesStatusState:appropiateActiveStatusState])) {
273 if ([menuItem state] != NSOnState) [menuItem setState:NSOnState];
275 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
278 //If there is not a status state, we are in a Custom state. Search for the correct Custom item.
279 if (menuItemStatusState) {
280 //If the menu item has an associated state, it's always off.
281 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
283 //If it doesn't, check the tag to see if it should be on or off.
284 if ([menuItem tag] == [appropiateActiveStatusState statusType]) {
285 if ([menuItem state] != NSOnState) [menuItem setState:NSOnState];
287 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
292 /* General menu items */
293 NSSet *allActiveStatusStates = [[adium statusController] allActiveStatusStates];
294 int onState = (([allActiveStatusStates count] == 1) ? NSOnState : NSMixedState);
296 if (menuItemStatusState) {
297 //If this menu item has a status state, set it to the right on state if that state is active
298 if ([allActiveStatusStates containsObject:menuItemStatusState] ||
299 ([menuItemStatusState isKindOfClass:[AIStatusGroup class]] &&
300 [(AIStatusGroup *)menuItemStatusState enclosesStatusStateInSet:allActiveStatusStates])) {
301 if ([menuItem state] != onState) [menuItem setState:onState];
303 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
306 //If it doesn't, check the tag to see if it should be on or off by looking for a matching custom state
307 NSEnumerator *activeStatusStatesEnumerator = [allActiveStatusStates objectEnumerator];
308 NSSet *flatStatusSet = [[adium statusController] flatStatusSet];
309 AIStatus *statusState;
310 BOOL foundCorrectStatusState = NO;
312 while (!foundCorrectStatusState && (statusState = [activeStatusStatesEnumerator nextObject])) {
313 /* We found a custom match if our array of menu item states doesn't contain this state and
314 * its statusType matches the menuItem's tag.
316 foundCorrectStatusState = (![flatStatusSet containsObject:statusState] &&
317 ([menuItem tag] == [statusState statusType]));
320 if (foundCorrectStatusState) {
321 if ([menuItem state] != NSOnState) [menuItem setState:onState];
323 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
328 [stateMenuItemsAlreadyValidated addObject:menuItem];
335 * @brief Select a state menu item
337 * Invoked by a state menu item, sets the state corresponding to the menu item as the active state.
339 * If the representedObject NSDictionary has an @"AIAccount" object, set the state just for the appropriate AIAccount.
340 * Otherwise, set the state globally.
342 - (void)selectState:(id)sender
344 NSDictionary *dict = [sender representedObject];
345 AIStatusItem *statusItem = [dict objectForKey:@"AIStatus"];
346 AIAccount *account = [dict objectForKey:@"AIAccount"];
348 if ([statusItem isKindOfClass:[AIStatusGroup class]]) {
349 statusItem = [(AIStatusGroup *)statusItem anyContainedStatus];
352 /* Random undocumented feature of the moment... hold option and select a state to bring up the custom status window
353 * for modifying and then setting it.
355 if ([NSEvent optionKey]) {
356 [AIEditStateWindowController editCustomState:(AIStatus *)statusItem
357 forType:[statusItem statusType]
361 notifyingTarget:[adium statusController]];
367 shouldRebuild = [[adium statusController] removeIfNecessaryTemporaryStatusState:[account statusState]];
368 [account setStatusState:(AIStatus *)statusItem];
370 //Enable the account if it isn't currently enabled
371 if (![account enabled] && [statusItem statusType] != AIOfflineStatusType) {
372 [account setEnabled:YES];
376 //Rebuild our menus if there was a change
377 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
381 [[adium statusController] setActiveStatusState:(AIStatus *)statusItem];
387 * @brief Select the custom state menu item
389 * Invoked by the custom state menu item, opens a custom state window.
390 * If the representedObject NSDictionary has an @"AIAccount" object, configure just for the appropriate AIAccount.
391 * Otherwise, configure globally.
393 - (IBAction)selectCustomState:(id)sender
395 NSDictionary *dict = [sender representedObject];
396 AIAccount *account = [dict objectForKey:@"AIAccount"];
397 AIStatusType statusType = [sender tag];
398 AIStatus *baseStatusState;
401 baseStatusState = [account statusState];
403 baseStatusState = [[adium statusController] activeStatusState];
406 /* If we are going to a custom state of a different type, we don't want to prefill with baseStatusState as it stands.
407 * Instead, we load the last used status of that type.
409 if (([baseStatusState statusType] != statusType)) {
410 NSDictionary *lastStatusStates = [[adium preferenceController] preferenceForKey:@"LastStatusStates"
411 group:PREF_GROUP_STATUS_PREFERENCES];
412 NSData *lastStatusStateData = [lastStatusStates objectForKey:[[NSNumber numberWithInt:statusType] stringValue]];
413 AIStatus *lastStatusStateOfThisType = (lastStatusStateData ?
414 [[NSKeyedUnarchiver unarchiveObjectWithData:lastStatusStateData] objectAtIndex:0] :
417 baseStatusState = [[lastStatusStateOfThisType retain] autorelease];
420 /* Don't use the current status state as a base, and when going from Away to Available, don't autofill the Available
421 * status message with the old away message.
423 if ([baseStatusState statusType] != statusType) {
424 baseStatusState = nil;
427 [AIEditStateWindowController editCustomState:baseStatusState
432 notifyingTarget:[adium statusController]];
436 #pragma mark Class methods
437 + (NSMenu *)staticStatusStatesMenuNotifyingTarget:(id)target selector:(SEL)selector
439 NSMenu *statusStatesMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
440 NSEnumerator *enumerator;
441 AIStatus *statusState;
442 AIStatusType currentStatusType = AIAvailableStatusType;
443 NSMenuItem *menuItem;
445 [statusStatesMenu setMenuChangedMessagesEnabled:NO];
446 [statusStatesMenu setAutoenablesItems:NO];
448 if (!target && !selector) {
449 //Need to set a target and action for items with submenus (AIStatusGroups) to be selectable... so if we're not given one, set one.
451 selector = @selector(dummyAction:);
454 /* Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
455 * are grouped together.
457 enumerator = [[[[AIObject sharedAdiumInstance] statusController] sortedFullStateArray] objectEnumerator];
458 while ((statusState = [enumerator nextObject])) {
459 AIStatusType thisStatusType = [statusState statusType];
461 //We treat Invisible statuses as being the same as Away for purposes of the menu
462 if (thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
464 if (currentStatusType != thisStatusType) {
465 //Add a divider between each type of status
466 [statusStatesMenu addItem:[NSMenuItem separatorItem]];
467 currentStatusType = thisStatusType;
470 menuItem = [[NSMenuItem alloc] initWithTitle:[AIStatusMenu titleForMenuDisplayOfState:statusState]
475 [menuItem setImage:[statusState menuIcon]];
476 [menuItem setTag:[statusState statusType]];
477 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
478 forKey:@"AIStatus"]];
479 if ([statusState isKindOfClass:[AIStatus class]]) {
480 [menuItem setToolTip:[statusState statusMessageTooltipString]];
484 [menuItem setSubmenu:[(AIStatusGroup *)statusState statusSubmenuNotifyingTarget:target
488 [statusStatesMenu addItem:menuItem];
492 [statusStatesMenu setMenuChangedMessagesEnabled:YES];
494 return [statusStatesMenu autorelease];
498 * @brief Determine a string to use as a menu title
500 * This method truncates a state title string for display as a menu item.
501 * Wide menus aren't pretty and may cause crashing in certain versions of OS X, so all state
502 * titles should be run through this method before being used as menu item titles.
504 * @param statusState The state for which we want a title
506 * @result An appropriate NSString title
508 + (NSString *)titleForMenuDisplayOfState:(AIStatusItem *)statusState
510 NSString *title = [statusState title];
512 /* Why plus 3? Say STATE_TITLE_MENU_LENGTH was 7, and the title is @"ABCDEFGHIJ".
513 * The shortened title will be @"ABCDEFG..." which looks to be just as long - even
514 * if the ellipsis is an ellipsis character and therefore technically two characters
515 * shorter. Better to just use the full string, which appears as being the same length.
517 if ([title length] > (STATE_TITLE_MENU_LENGTH + 3)) {
518 title = [title stringWithEllipsisByTruncatingToLength:STATE_TITLE_MENU_LENGTH];
524 + (void)dummyAction:(id)sender {};